iT邦幫忙

2023 iThome 鐵人賽

DAY 18
0
Modern Web

30天React練功坊-攻克常見實務/面試問題系列 第 18

30天React練功坊-攻克常見實務/面試問題 Day18:UI flicker issue with useEffect

  • 分享至 

  • xImage
  •  
tags: ItIron2023 react

前言

昨天我們看了一個誤用useEffect的例子,了解到其實有些東西你不需要靠useEffect也能達成,今天我們則來看一個你可能會用useEffect的例子,但是否這是最佳解則有待商榷囉!

本日題目

首先一樣請你看一下這個codesandbox以及下方的gif檔案。

day18-demo-gif

今天我們碰到一個蠻有趣的情況,你先向後端那邊請求了一長串的資料,但由於某些商業需求你需要在使用者開啟頁面時直接從最新的資料看起,因此他們希望你一開始就捲動到最底下,這類的需求最常見的情況我想莫過於聊天室訊息的場景了,你總是希望使用者能從最新的對話開始看到,仔細觀察一下今天的gif檔案,你很明顯的有達到指定的需求,確實資料fetch完之後它有捲動到最下方,但中間有個很明顯的抖動(flicker),請觀察以下程式碼

const App = () => {
  const [comments, setComments] = useState([]);
  const containerRef = useRef(null);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/comments")
      .then((res) => res.json())
      .then((data) => {
        setTimeout(() => setComments(data), 2000);
      });
  }, []);

  useEffect(() => {
    if (containerRef.current) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  }, [comments]);

  return (
    <>
      <h1>UI flicker issue with useEffect</h1>
      <div ref={containerRef} style={{ height: "400px", overflowY: "scroll" }}>
        {comments.map((comment) => (
          <div
            key={comment.id}
            style={{ padding: "10px", borderBottom: "1px solid #ccc" }}
          >
            <h4>{comment.name}</h4>
            <p>{comment.body}</p>
          </div>
        ))}
      </div>
    </>
  );
};

並在滿足下列條件的同時試著解決這個問題。

1. 請勿刪除setTimeout 2000ms的程式碼,這是用來模擬大型組件或是複雜的DOM操作造成渲染費時的情況。
2. 請勿更改任何css,改為smooth雖然是一種解法,但並不滿足這次題目的要求。

解答與基本解釋

這個題目很微妙,說難也不難、說很罕見卻也不是這麼罕見,但至少我很不巧的在實務中遇到數次這樣的情況,那麼首先我們要先了解這個組件到底發生什麼事情、為什麼會有這樣的行為。
大致上來說組件內按順序發生了以下的事情

  1. 第一個useEffect發出api請求
  2. 改變state觸發重新渲染,觸發的當下仍沒有任何資料,因此第二個useEffect坦白說沒幹事
  3. 第二次渲染時第一個useEffect就罷工了,第二個useEffect則執行setTimeout的捲動程式碼,在兩秒後捲動到最下方

再次強調,兩秒的延遲僅是一種模擬,這個範例比較麻煩的地方在於它需要很大的渲染成本才能看出問題.了解以上流程後你會發現這完全就是預期中的行為,useEffect會在渲染階段後執行,既然這樣你先看到完整畫面才去執行捲動自然就是合理的,但我們今天要做的是在DOM操作後才讓使用者看到完成的畫面,這就很明顯不是useEffect能做到的事情了,就像我們剛講過的,useEffect的執行都是在你已經看到畫面了。

雖然這種情況較為罕見,但React還是替我們這些偏執狂提供了一個工具-useLayoutEffect。 這玩意與useEffect極為相似,都有著相同的結構與相似的效果,唯一差別在於他們倆執行的時間點,而這關鍵的差異就是這次的解法。

兩者都會在render階段完成後執行,但useLayoutEffect會在瀏覽器實際Paint到畫面前執行,而useEffect則會在Paint之後執行,也就是說若你有任何DOM操作的結果想直接讓使用者看到,比方說根據某個容器去改高度、在某種條件下改某個div的背景顏色等,這些都屬於useLayoutEffect適用情況,整個流程會是

  1. render完成
  2. useLayoutEffect執行
  3. 瀏覽器Paint畫面
  4. useEffect執行

至於為什麼他能擋在瀏覽器Paint前幹活呢?最根本的原因在於它是同步(synchronous)的操作,也因此這玩意會block整個渲染的過程,務必不要在這個hook中使用太昂貴的操作。

上面如果都聽懂了,那我想你應該知道該怎麼改了,把原本的hook換成useLayoutEffect即可,如下。

useLayoutEffect(() => {
  if (containerRef.current) {
    containerRef.current.scrollTop = containerRef.current.scrollHeight;
  }
}, [comments]);

總結

今天我們看了一個相當有趣的例子,這個hook我想多半人或多或少都有聽過,但實際有使用的絕對是少之又少,99%的情況你都不需要用到這玩意,絕大多數情境僅需要useEffect而非出動useLayoutEffect,但如果你真的知道自己在做什麼、也確實了解需要處理的情境,那這玩意確實有它適用的地方!至少我就遇過幾次!那麼我們明天見囉!

本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!


上一篇
30天React練功坊-攻克常見實務/面試問題 Day17: useEffect to the rescue, or is it?
下一篇
30天React練功坊-攻克常見實務/面試問題 Day19: useState initial value not update after re-render
系列文
30天React練功坊-攻克常見實務/面試問題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言